/**
 * 
 */
package speculoos.jndi.mappers;

import java.util.Collection;
import java.util.Map;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchResult;

import speculoos.core.Mapper;
import speculoos.core.MapperException;
import speculoos.utils.TypeHelper;

/**
 * This class handles mapping from one SearchResult object to some objcet
 * instance using a LDAP to Java names map and a class. It is constructed from a
 * Map object containing pairs of strings:
 * <ul>
 * <li>The key string is an attribute name in LDAP space,</li>
 * <li>The value string is an attribute name in Java space.</li>
 * </ul>
 * The second element for construction is a Class object. The given class must
 * have a default constructor and follows the javabeans naming conventions.
 * 
 * @author abailly
 */
public class OutputMap implements Mapper {

	/*
	 * map of attribute names
	 */
	private Map outputMap;

	/*
	 * instance constructed by this map
	 */
	private Class klass;

	/**
	 * Create OutputMap for given class and map.
	 * 
	 * @param cls
	 * @param map
	 */
	public OutputMap(Class cls, Map map) {
		this.klass = cls;
		this.outputMap = map;
	}
	
	/**
	 * Default Constructor.
	 * Note that without further configuration, this constructor 
	 * will probably make map method fails.
	 */
	public OutputMap() {
		
	}

	/**
	 * Updates this OutputMap object with the content of the given Map which is
	 * a Map<String,String> instanceof LDAP names to Java names.
	 * 
	 * @param output
	 *            a Map<String,String> instance. May not be null.
	 */
	public void setOutputMap(Map output) {
		this.outputMap = output;
	}

	/**
	 * @return Returns the outputMap.
	 */
	public Map getOutputMap() {
		return outputMap;
	}

	/**
	 * @return Returns the klass.
	 */
	public Class getKlass() {
		return klass;
	}

	/**
	 * @param klass
	 *            The klass to set.
	 */
	public void setKlass(Class klass) {
		this.klass = klass;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see speculoos.core.Mapper#map(java.lang.Object, java.util.Map)
	 */
	public Object map(Object input, Map param) throws MapperException {
		try {
			return makeObject((SearchResult) input, param);
		} catch (NamingException ex) {
			throw new MapperException(getName() + ": JNDI error ", ex);
		} catch (InstantiationException e) {
			throw new MapperException(getName() + ": Cannot instantiate "
					+ klass, e);
		} catch (IllegalAccessException e) {
			throw new MapperException(getName()
					+ ": Cannot access empty contsructor for " + klass, e);
		}
	}

	/**
	 * @param input
	 * @param param
	 * @return
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 * @throws NamingException
	 * @throws MapperException
	 */
	private Object makeObject(Object input, Map param)
			throws InstantiationException, IllegalAccessException,
			NamingException, MapperException {
		TypeHelper th = TypeHelper.instance;
		Object ret = klass.newInstance();
		SearchResult res = (SearchResult) input;
		Attributes attrs = res.getAttributes();
		for (NamingEnumeration e = attrs.getAll(); e.hasMore();) {
			Attribute attr = (Attribute) e.next();
			String id = attr.getID();
			String n = (String) outputMap.get(id);
			if (n == null)
				continue;
			/* type of field */
			Class fcls = th.getClass(ret, n);
			if (fcls == null) {
				continue;
			}
			/* handling of multiple values */
			if (attr.size() > 1 || Collection.class.isAssignableFrom(fcls)) {
				try {
					Collection c = (Collection) th.get(ret, n);
					if (c == null) {
						throw new MapperException(getName()
								+ ": collection field '" + n
								+ "' is not initialized in " + klass);
					}
					/* corresponding field must be a collection */
					for (NamingEnumeration ne = attr.getAll(); ne.hasMore();) {
						c.add(ne.next());
					}
				} catch (ClassCastException ex) {
					throw new MapperException(getName() + ": field " + n
							+ " is not a collection in " + klass);
				}
			} else
				th.setString(ret, n, (String) attr.get());
		}
		/* try to set the DN attribute if it exists */
		String dn = (String) outputMap.get("__DN__");
		if (dn != null) {
			String rname = res.getName();
			if (!"".equals(rname)) {
				if(res.isRelative()) {
					rname = rname + "," + param.get("root");
				}
			} else {
				rname = param.get("root").toString();
			}
			th.set(ret, dn, rname);
		}
		return ret;
	}

	public String getName() {
		return "OutputMap";
	}

	/**
	 * Factory method for creating output maps given a type.
	 * This method gives the system the opportunity to optimize
	 * the output generation code for given class insstances. 
	 * It tries to lookup a Class instance named <code>OutputMapXXX</code> 
	 * where <code>XXX</code> is the simple name of <code>cls</code>, 
	 * in the same package.
	 * 
	 * @param cls a Class instance. may not be null.
	 * @param map a Map < String,String> instance from LDAP names to Java nameS. May
	 * not be null.
	 * @return an instanceof OutputMap possibly tailored to the 
	 * given class.
	 */
	public static OutputMap create(Class cls, Map map) {
		if(map == null || cls == null)
			throw new IllegalArgumentException("Cannot create output map from null map or null Class");
		/* compute class name */
		String cln = cls.getPackage().getName() + ".AttributesFrom"+cls.getName().substring(cls.getPackage().getName().length());
		/* load it */
		try {
			Class attrs = Class.forName(cln);
			return (OutputMap) attrs.newInstance();
		}catch(Exception e) {
			return new OutputMap(cls,map);
		}
	}

}
